package com.meego.common.crypto;

import com.meego.common.crypto.KeyStore.KeyStoreType;
import com.meego.common.log.GA;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.StringTokenizer;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * Triple DES is the common name for the Triple Data Encryption Algorithm block
 * cipher, which applies the Data Encryption Standard (DES) cipher algorithm
 * three times to each data block.
 *
 * @author OPS team
 *
 */
public class TripleDes {

    private static TripleDes crypto3DES;
    private String fullAlgorithm = "DESede/CBC/PKCS5Padding";//"DESede/CBC/PKCS5Padding";
    private String algorithm = "DESede";
    private String mdAlgorithm = "MD5"; //SHA-1 SHA-256
    private String charset = "UTF-8";
    private String key;

    public static TripleDes getInstance() {
        if (crypto3DES == null) {
            createInstance();
        }
        return crypto3DES;
    }

    private static synchronized void createInstance() {
        if (crypto3DES == null) {
            crypto3DES = new TripleDes();
        }
    }

    public void loadCrypto3DES(String fullAlgorithm, String mdAlgoorithm, String charset, byte[] iv) {
        if (fullAlgorithm != null && !fullAlgorithm.isEmpty()) {
            this.fullAlgorithm = fullAlgorithm;
            StringTokenizer st = new StringTokenizer(fullAlgorithm, "/");
            this.algorithm = st.nextToken();
        }
        if (mdAlgorithm != null && !mdAlgorithm.isEmpty()) {
            this.mdAlgorithm = mdAlgoorithm;
        }
        if (charset != null && !charset.isEmpty()) {
            this.charset = charset;
        }
    }

    protected TripleDes() {
        String desKey = "";
        try {
            desKey = CryptoKey.getInstance().getDesKey(KeyStoreType.STK, 0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (!desKey.isEmpty()) {
            key = desKey;
        }
    }

    public TripleDes(String algorithm) {
        this.algorithm = algorithm;
    }

    public String getFullAlgorithm() {
        return fullAlgorithm;
    }

    /**
     * A transformation is of the form: <ul> <li>"algorithm/mode/padding"
     * or</li> <li>"algorithm" </li> </ul>
     *
     * @param fullAlgorithm support the following standard Cipher
     * transformations with the keysizes in parentheses: <ul>
     * <li>AES/CBC/NoPadding (128) </li> <li>AES/CBC/PKCS5Padding (128) </li>
     * <li>AES/ECB/NoPadding (128) </li> <li>AES/ECB/PKCS5Padding (128) </li>
     * <li>DES/CBC/NoPadding (56) </li> <li>DES/CBC/PKCS5Padding (56) </li>
     * <li>DES/ECB/NoPadding (56) </li> <li>DES/ECB/PKCS5Padding (56) </li>
     * <li>DESede/CBC/NoPadding (168) </li> <li>DESede/CBC/PKCS5Padding (168)
     * </li> <li>DESede/ECB/NoPadding (168) </li> <li>DESede/ECB/PKCS5Padding
     * (168) </li> <li>RSA/ECB/PKCS1Padding (1024, 2048) </li>
     * <li>RSA/ECB/OAEPWithSHA-1AndMGF1Padding (1024, 2048) </li>
     * <li>RSA/ECB/OAEPWithSHA-256AndMGF1Padding (1024, 2048)</li> </ul>
     */
    public void setFullAlgorithm(String fullAlgorithm) {
        this.fullAlgorithm = fullAlgorithm;
    }

    public String getAlgorithm() {
        return algorithm;
    }

    public void setAlgorithm(String algorithm) {
        this.algorithm = algorithm;
    }

    /**
     * @param mdAlgorithm MessageDigest Algorithm, support the following
     * standard MessageDigest algorithms: <ul> <li>MD5</li> <li>SHA-1 </li>
     * <li>SHA-256</li> </ul>
     */
    public void setMdAlgorithm(String mdAlgorithm) {
        this.mdAlgorithm = mdAlgorithm;
    }

    public String getMdAlgorithm() {
        return mdAlgorithm;
    }

    /**
     * @param charset Support: <br /> US-ASCII &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
     * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the
     * Unicode character set<br /> ISO-8859-1 &nbsp;&nbsp; ISO Latin Alphabet
     * No. 1, a.k.a. ISO-LATIN-1<br /> UTF-8
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Eight-bit
     * UCS Transformation Format<br /> UTF-16BE &nbsp;&nbsp;&nbsp;&nbsp;
     * Sixteen-bit UCS Transformation Format, big-endian byte order<br />
     * UTF-16LE &nbsp;&nbsp;&nbsp;&nbsp; Sixteen-bit UCS Transformation Format,
     * little-endian byte order<br /> UTF-16
     * &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16-bit UCS
     * Transformation Format, byte order identified by an optional byte-order
     * mark
     */
    public void setCharset(String charset) {
        this.charset = charset;
    }

    public String getCharset() {
        return charset;
    }

    private String base64Enc(String message, String pass) throws Exception {
        String result = "";
        try {
            MessageDigest md = MessageDigest.getInstance(mdAlgorithm);//support MD5 SHA-1 SHA-256 
            final byte[] digestOfPassword = md.digest(pass.getBytes(charset));
            final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
            for (int j = 0, k = 16; j < 8;) {
                keyBytes[k++] = keyBytes[j++];
            }

            final SecretKey seck = new SecretKeySpec(keyBytes, algorithm);
            final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
            final Cipher cipher = Cipher.getInstance(fullAlgorithm);
            cipher.init(Cipher.ENCRYPT_MODE, seck, iv);

            final byte[] plainTextBytes = message.getBytes(charset);
            final byte[] cipherText = cipher.doFinal(plainTextBytes);
            BASE64Encoder encoder = new BASE64Encoder();
            result = encoder.encode(cipherText);
        } catch (Exception e) {
            GA.crypto.error("Crypto3DES: Encrypt data fail", e);
            throw e;
        }
        return result;
    }

    private String enc(String message, String pass) throws Exception {
        String result = "";
        try {
            final MessageDigest md = MessageDigest.getInstance(mdAlgorithm);//support MD5 SHA-1 SHA-256 
            final byte[] digestOfPassword = md.digest(pass.getBytes(charset));
            final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
            for (int j = 0, k = 16; j < 8;) {
                keyBytes[k++] = keyBytes[j++];
            }

            final SecretKey seck = new SecretKeySpec(keyBytes, algorithm);
            final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
            final Cipher cipher = Cipher.getInstance(fullAlgorithm);
            cipher.init(Cipher.ENCRYPT_MODE, seck, iv);

            final byte[] plainTextBytes = message.getBytes(charset);
            final byte[] cipherText = cipher.doFinal(plainTextBytes);
            result = new String(cipherText, charset);
        } catch (Exception e) {
            GA.crypto.error("Crypto3DES: Encrypt data fail", e);
            throw e;
        }
        return result;
    }

    private byte[] enc(byte[] message, byte[] pass) throws Exception {
        byte[] cipherText;
        try {
            final MessageDigest md = MessageDigest.getInstance(mdAlgorithm);//support MD5 SHA-1 SHA-256 
            final byte[] digestOfPassword = md.digest(pass);
            final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
            for (int j = 0, k = 16; j < 8;) {
                keyBytes[k++] = keyBytes[j++];
            }

            final SecretKey seck = new SecretKeySpec(keyBytes, algorithm);
            final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
            final Cipher cipher = Cipher.getInstance(fullAlgorithm);
            cipher.init(Cipher.ENCRYPT_MODE, seck, iv);

            cipherText = cipher.doFinal(message);
        } catch (Exception e) {
            GA.crypto.error("Crypto3DES: Encrypt data fail", e);
            throw e;
        }

        return cipherText;
    }

    private String base64Dec(String message, String pass) throws Exception {
        String result = "";
        try {
            final MessageDigest md = MessageDigest.getInstance(mdAlgorithm);
            final byte[] digestOfPassword = md.digest(pass.getBytes(charset));
            final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
            for (int j = 0, k = 16; j < 8;) {
                keyBytes[k++] = keyBytes[j++];
            }
            final SecretKey seck = new SecretKeySpec(keyBytes, algorithm);
            final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
            final Cipher decipher = Cipher.getInstance(fullAlgorithm);
            decipher.init(Cipher.DECRYPT_MODE, seck, iv);

            BASE64Decoder decoder = new BASE64Decoder();

            final byte[] plainText = decipher.doFinal(decoder.decodeBuffer(message));
            result = new String(plainText, charset);
        } catch (Exception e) {
            GA.crypto.error("Crypto3DES: Encrypt data fail", e);
            throw e;
        }

        return result;
    }

    private String dec(String message, String pass) throws Exception {
        String result = "";
        try {
            final MessageDigest md = MessageDigest.getInstance(mdAlgorithm);
            final byte[] digestOfPassword = md.digest(pass.getBytes(charset));
            final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
            for (int j = 0, k = 16; j < 8;) {
                keyBytes[k++] = keyBytes[j++];
            }
            final SecretKey seck = new SecretKeySpec(keyBytes, algorithm);
            final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
            final Cipher decipher = Cipher.getInstance(fullAlgorithm);
            decipher.init(Cipher.DECRYPT_MODE, seck, iv);

            final byte[] plainText = decipher.doFinal(message.getBytes(charset));
            result = new String(plainText, charset);
        } catch (Exception e) {
            GA.crypto.error("Crypto3DES: Encrypt data fail", e);
            throw e;
        }
        return result;
    }

    private byte[] dec(byte[] message, byte[] pass) throws Exception {
        byte[] plainText;
        try {
            final MessageDigest md = MessageDigest.getInstance(mdAlgorithm);
            final byte[] digestOfPassword = md.digest(pass);
            final byte[] keyBytes = Arrays.copyOf(digestOfPassword, 24);
            for (int j = 0, k = 16; j < 8;) {
                keyBytes[k++] = keyBytes[j++];
            }
            final SecretKey seck = new SecretKeySpec(keyBytes, algorithm);
            final IvParameterSpec iv = new IvParameterSpec(new byte[8]);
            final Cipher decipher = Cipher.getInstance(fullAlgorithm);
            decipher.init(Cipher.DECRYPT_MODE, seck, iv);

            plainText = decipher.doFinal(message);
        } catch (Exception e) {
            GA.crypto.error("Crypto3DES: Encrypt data fail", e);
            throw e;
        }
        return plainText;
    }

    public String base64Encrypt(String message, String pass) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        return crypto3DES.base64Enc(message, pass);
    }

    public String encrypt(String message, String pass) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        return crypto3DES.enc(message, pass);
    }

    public byte[] encrypt(byte[] message, byte[] pass) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        return crypto3DES.enc(message, pass);
    }

    public String base64Decrypt(String message, String pass) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        return crypto3DES.base64Dec(message, pass);
    }

    public String decrypt(String message, String pass) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        return crypto3DES.dec(message, pass);
    }

    public byte[] decrypt(byte[] message, byte[] pass) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        return crypto3DES.dec(message, pass);
    }

    public String reEncrypt(String data, String decryptKey, String encryptKey) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        data = crypto3DES.dec(data, decryptKey);
        return crypto3DES.enc(data, encryptKey);
    }

    public byte[] reEncrypt(byte[] data, byte[] decryptKey, byte[] encryptKey) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        data = crypto3DES.dec(data, decryptKey);
        return crypto3DES.enc(data, encryptKey);
    }

    public String reBase64Encrypt(String data, String decryptKey, String encryptKey) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        data = crypto3DES.base64Dec(data, decryptKey);
        return crypto3DES.base64Enc(data, encryptKey);
    }

    public String base64Encrypt(String message) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        if (key == null || key.isEmpty()) {
            CryptException t = new CryptException("Key not found", "3DES");
            GA.config.error("3DES key is not yet loaded", t);
            throw t;
        }
        return crypto3DES.base64Enc(message, key);
    }

    public String encrypt(String message) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES is not yet loaded", t);
            throw t;
        }
        if (key == null || key.isEmpty()) {
            CryptException t = new CryptException("Key not found", "3DES");
            GA.config.error("3DES key is not yet loaded", t);
            throw t;
        }
        return crypto3DES.enc(message, key);
    }

    public byte[] encrypt(byte[] message) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }

        if (key == null || key.isEmpty()) {
            CryptException t = new CryptException("Key not found", "3DES");
            GA.config.error("3DES key is not yet loaded", t);
            throw t;
        }
        return crypto3DES.enc(message, key.getBytes());
    }

    public String base64Decrypt(String message) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        if (key == null || key.isEmpty()) {
            CryptException t = new CryptException("Key not found", "3DES");
            GA.config.error("3DES key is not yet loaded", t);
            throw t;
        }
        return crypto3DES.base64Dec(message, key);
    }

    public String decrypt(String message) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        if (key == null || key.isEmpty()) {
            CryptException t = new CryptException("Key not found", "3DES");
            GA.config.error("3DES key is not yet loaded", t);
            throw t;
        }
        return crypto3DES.dec(message, key);
    }

    public byte[] decrypt(byte[] message) throws Exception {
        if (crypto3DES == null) {
            CryptException t = new CryptException("instance not yet created", "3DES");
            GA.config.error("Crypto3DES not yet loaded", t);
            throw t;
        }
        if (key == null || key.isEmpty()) {
            CryptException t = new CryptException("Key not found", "3DES");
            GA.config.error("3DES key is not yet loaded", t);
            throw t;
        }
        return crypto3DES.dec(message, key.getBytes());
    }

    public void setKey(String key) throws InvalidKeyException {
        if (key == null || key.isEmpty()) {
            InvalidKeyException t = new InvalidKeyException("Invalid 3DES key. 3DES key can not be <null> or empty");
            GA.crypto.error("Invalid Key", t);
            throw t;
        }
        this.key = key;
    }
}
